/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.client.veditor.model;

import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import javax.swing.text.Element;

/**
 * Calculate the next or next data v while traversing using the TAB or SHIFT TAB keys. Return an IteratorResult with the complete
 * v set, that shall be inserted and the common parentElement of these elements.
 */
class TreeIterator {

    DataModel model = null;
    VirtualBlock virtualBlock;

    public TreeIterator(VirtualElement root, DataModel model, VirtualBlock virtualBlock) {
        this.virtualBlock = virtualBlock;
        this.model = model;
        setDataToElement(root);
    }

    void setDataToElement(VirtualElement v) {
        if (v.isDataElement() && v.getAttributes().isDefined(DataModel.ATTRIBUTE_NAME_DATA))
            virtualBlock.dataToElement.put((String) v.getAttributes().getAttribute(DataModel.ATTRIBUTE_NAME_DATA), v);
        for (int i = 0; i < v.getElementCount(); i++) {
            setDataToElement(v.getVirtualElement(i));
        }
    }

    class IteratorResult {

        HashMap<VirtualElement, VirtualElementInstance> path = new HashMap<VirtualElement, VirtualElementInstance>();
        VirtualElementInstance last = null;
        VirtualElementInstance highlight = null;
        VirtualElementInstance commonParent = null;

        public IteratorResult() {
        }

        public IteratorResult(VirtualElementInstance start) {
            commonParent = start.getParentInstance();
        }

        @Override
        public Object clone() {
            IteratorResult result = new IteratorResult();
            result.path = new HashMap<VirtualElement, VirtualElementInstance>();
            Iterator<VirtualElement> i = path.keySet().iterator();
            while (i.hasNext()) {
                VirtualElement key = i.next();
                result.path.put(key, path.get(key));
            }
            result.last = last;
            result.highlight = highlight;
            result.commonParent = commonParent;
            return result;
        }

        VirtualElementInstance[] getPathArray(VirtualElementInstance v) {
            LinkedList<VirtualElementInstance> pathList = new LinkedList<VirtualElementInstance>();
            while (v != null) {
                pathList.addFirst(v);
                v = v.getParentInstance();
            }
            return pathList.toArray(new VirtualElementInstance[pathList.size()]);
        }

        public void add(VirtualElementInstance v) throws Exception {
            last = v;
            if (!virtualBlock.cannotHighlight(v))
                highlight = v;
            VirtualElementInstance parent = v.getParentInstance();
            if (parent == null) {
                addSingle(v);
                return;
            }
            for (int i = 0; i < parent.getElementCount(); i++) {
                VirtualElement child = parent.getVirtualElement(i);
                if (child == v.getVirtualElement())
                    addSingle(v);
                else if (virtualBlock.insertAllChildren(parent) || !virtualBlock.hasData(child) && !path.containsKey(child))
                    addTree(child, parent);
            }
        }

        /**
         * Add the virtual element and its whole subtree
         *
         * @param v the virtual element
         * @param parent its parent instance
         */
        protected void addTree(VirtualElement v, VirtualElementInstance parent) {
            VirtualElementInstance vi = new VirtualElementInstance(v, virtualBlock);
            vi.setParentInstance(parent);
            addSingle(vi);
            for (int i = 0; i < v.getElementCount(); i++) {
                addTree(v.getVirtualElement(i), vi);
            }
        }

        /**
         * Add a singe virtual instance.
         *
         * @param v the virtual instance
         */
        protected void addSingle(VirtualElementInstance v) {
            if (commonParent != null) {
                VirtualElementInstance[] commonParentPath = getPathArray(commonParent);
                VirtualElementInstance[] vPath = getPathArray(v.getParentInstance());
                for (int i = 0; i < commonParentPath.length && i < vPath.length; i++) {
                    if (vPath[i].getVirtualElement() == commonParentPath[i].getVirtualElement())
                        commonParent = vPath[i]; // should match always in the root virtual v (the div)
                }
            }
            else
                commonParent = v.getParentInstance();
            path.put(v.getVirtualElement(), v);
        }

        public VirtualElementInstance getCommonParent() {
            return commonParent;
        }

        public VirtualElementInstance getLast() {
            return last;
        }

        public VirtualElementInstance getHighlight() {
            return highlight;
        }

        public void consumeHighlight() {
            this.highlight = null;
        }

        public boolean contains(VirtualElement v, int eIndex, Element parentElement) {
            if (!path.containsKey(v))
                return false;
            if (v.isMultiple() && eIndex >= 0 && parentElement != null) {
                Element e = parentElement.getElement(eIndex);
                if (!path.get(v).equals(e))
                    return false;
            }
            if (parentElement == null)
                return true;
            VirtualElementInstance parent = path.get(v).getParentInstance();
            while (parent != null) {
                // if the parent is a multiple occuring v, has it the same multipleIndex position?
                if (!parent.equals(parentElement))
                    return false;
                parent = parent.getParentInstance();
                parentElement = parentElement.getParentElement();
            }
            return true;
        }

        public void remove(VirtualElement v) {
            path.remove(v);
        }
    }

    protected VirtualElementInstance down(VirtualElementInstance v, boolean forward) {
        if (v.getElementCount() == 0)
            return null;
        if (forward)
            return v.childInstance(0);
        else {
            VirtualElementInstance result = v.childInstance(v.getElementCount() - 1);
            result.setMultipleIndex(VirtualElementInstance.MI_LAST);
            return result;
        }
    }

    protected boolean hasNext(VirtualElementInstance v) {
        if (virtualBlock.isRowSelector(v)) // no elements will be edited after a row selector
            return false;
        if (v.isMultiple() && !v.isLast())
            return true;
        int index = model.getChildIndex(v.getVirtualElement());
        if (index < v.getParent().getElementCount() - 1)
            return true;
        return false;
    }

    protected boolean hasPrevious(VirtualElementInstance v) {
        if (v.isMultiple() && v.getMultipleIndex() > 0)
            return true;
        int index = model.getChildIndex(v.getVirtualElement());
        if (index > 0)
            return true;
        return false;
    }

    /**
     * Return the successor v, depending on the direction (forward) either the next or next leaf data v.
     * This will traverse the tree in depth first order.
     *
     * @param e
     * @param r
     * @param forward
     * @return
     */
    public IteratorResult successorDataElement(Element e, Element remove, boolean forward) throws Exception {
        VirtualElementInstance before = virtualBlock.getCorrespondingVirtualElementInstance(e);
        boolean skipFirst = e == remove && ((forward && before.isLast()) || (!forward && before.getMultipleIndex() == 0));
        IteratorResult result = new IteratorResult(before);
        if (model.isLeaf(before) && before.isMultiple()) {
            // multiply occurring leaf: return leaf again
            result.add(before.nextInstance());
            return result;
        }
        VirtualElementInstance v = before;
        do {
            if (v != before && v.getElementCount() > 0)
                v = down(v, forward);
            else if (forward && hasNext(v))
                if (v == before)
                    v = v.nextInstance(skipFirst);
                else
                    v = v.nextInstance();
            else if (!forward && hasPrevious(v))
                if (v == before)
                    v = v.previousInstance(skipFirst);
                else
                    v = v.previousInstance();
            else
                v = up(v, remove, forward);
            result.add(v);
        } while (!(model.isLeaf(v) && model.isData(v) && virtualBlock.getModel().canEdit(v)));
        return result;
    }

    /**
     * Moves upward at the end of a siblings line, if necessary multiple times.
     * Returns the next or next v of the final parentElement v.
     *
     * @param v
     * @param forward
     * @return
     */
    protected VirtualElementInstance up(VirtualElementInstance v, Element remove, boolean forward) {
        v = v.getParentInstance();
        VirtualElementInstance found = null;
        while (found == null) {
            if (v.getVirtualElement() == virtualBlock.root)
                found = new VirtualElementInstance(virtualBlock.root, virtualBlock);
            else if (v.isMultiple() && virtualBlock.getModel().canEdit(v) && !v.equals(remove))
                if (forward)
                    found = v.nextInstance();
                else
                    found = v.previousInstance();
            else if (forward && hasNext(v))
                found = v.nextInstance();
            else if (!forward && hasPrevious(v))
                found = v.previousInstance();
            else
                v = v.getParentInstance();
        }
        return found;
    }

    public IteratorResult successorDataElement(int pos, Element remove, boolean forward) throws Exception {
        Element e = model.getDataElement(pos);
        return successorDataElement(e, remove, forward);
    }

    public IteratorResult nextDataElement(int pos) throws Exception {
        return successorDataElement(pos, null, true);
    }

    public IteratorResult previousDataElement(int pos) throws Exception {
        return successorDataElement(pos, null, false);
    }

    public IteratorResult nextDataElement(Element e) throws Exception {
        return successorDataElement(e, null, true);
    }

    public IteratorResult previousDataElement(Element e) throws Exception {
        return successorDataElement(e, null, false);
    }
}
